﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Ckknight.StateMachine
{
    /// <summary>
    /// A static class of helper methods to create new StateMachineBuilders.
    /// </summary>
    public static class StateMachineBuilder
    {
        #region Public methods

        /// <summary>
        /// Return a new <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/> where the TModel is a simple <see langword="object"/>, likely to be disregarded.
        /// </summary>
        /// <typeparam name="TState">The type used to represent the state of the machine.</typeparam>
        /// <typeparam name="TTrigger">The type used to represent triggers which cause transitions within the machine.</typeparam>
        /// <returns>A new StateMachineBuilder</returns>
        public static StateMachineBuilder<TState, TTrigger, object> Create<TState, TTrigger>()
        {
            return Create<TState, TTrigger, object>(null, null);
        }

        /// <summary>
        /// Return a new <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/> where the TModel is a simple <see langword="object"/>, likely to be disregarded with the provided <paramref name="stateComparer"/>.
        /// </summary>
        /// <typeparam name="TState">The type used to represent the state of the machine.</typeparam>
        /// <typeparam name="TTrigger">The type used to represent triggers which cause transitions within the machine.</typeparam>
        /// <param name="stateComparer">A comparer to test whether two states are equivalent. Can be null.</param>
        /// <returns>A new StateMachineBuilder</returns>
        public static StateMachineBuilder<TState, TTrigger, object> Create<TState, TTrigger>(IEqualityComparer<TState> stateComparer)
        {
            return Create<TState, TTrigger, object>(stateComparer, null);
        }

        /// <summary>
        /// Return a new <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/> where the TModel is a simple <see langword="object"/>, likely to be disregarded with the provided <paramref name="stateComparer"/> and <paramref name="triggerComparer"/>.
        /// </summary>
        /// <typeparam name="TState">The type used to represent the state of the machine.</typeparam>
        /// <typeparam name="TTrigger">The type used to represent triggers which cause transitions within the machine.</typeparam>
        /// <param name="stateComparer">A comparer to test whether two states are equivalent. Can be null.</param>
        /// <param name="triggerComparer">A comparer to test whether two triggers are equivalent. Can be null.</param>
        /// <returns>A new StateMachineBuilder</returns>
        public static StateMachineBuilder<TState, TTrigger, object> Create<TState, TTrigger>(IEqualityComparer<TState> stateComparer, IEqualityComparer<TTrigger> triggerComparer)
        {
            return Create<TState, TTrigger, object>(stateComparer, triggerComparer);
        }

        /// <summary>
        /// Return a new <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/>.
        /// </summary>
        /// <typeparam name="TState">The type used to represent the state of the machine.</typeparam>
        /// <typeparam name="TTrigger">The type used to represent triggers which cause transitions within the machine.</typeparam>
        /// <typeparam name="TModel">The type which the machine will carry with it and can be used to store information or react differently.</typeparam>
        /// <returns>A new StateMachineBuilder</returns>
        public static StateMachineBuilder<TState, TTrigger, TModel> Create<TState, TTrigger, TModel>()
        {
            return Create<TState, TTrigger, TModel>(null, null);
        }

        /// <summary>
        /// Return a new <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/> with the provided <paramref name="stateComparer"/>.
        /// </summary>
        /// <typeparam name="TState">The type used to represent the state of the machine.</typeparam>
        /// <typeparam name="TTrigger">The type used to represent triggers which cause transitions within the machine.</typeparam>
        /// <typeparam name="TModel">The type which the machine will carry with it and can be used to store information or react differently.</typeparam>
        /// <param name="stateComparer">A comparer to test whether two states are equivalent. Can be null.</param>
        /// <returns>A new StateMachineBuilder</returns>
        public static StateMachineBuilder<TState, TTrigger, TModel> Create<TState, TTrigger, TModel>(IEqualityComparer<TState> stateComparer)
        {
            return Create<TState, TTrigger, TModel>(stateComparer, null);
        }

        /// <summary>
        /// Return a new <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/> with the provided <paramref name="stateComparer"/> and <paramref name="triggerComparer"/>.
        /// </summary>
        /// <typeparam name="TState">The type used to represent the state of the machine.</typeparam>
        /// <typeparam name="TTrigger">The type used to represent triggers which cause transitions within the machine.</typeparam>
        /// <typeparam name="TModel">The type which the machine will carry with it and can be used to store information or react differently.</typeparam>
        /// <param name="stateComparer">A comparer to test whether two states are equivalent. Can be null.</param>
        /// <param name="triggerComparer">A comparer to test whether two triggers are equivalent. Can be null.</param>
        /// <returns>A new StateMachineBuilder</returns>
        public static StateMachineBuilder<TState, TTrigger, TModel> Create<TState, TTrigger, TModel>(IEqualityComparer<TState> stateComparer, IEqualityComparer<TTrigger> triggerComparer)
        {
            return new StateMachineBuilder<TState, TTrigger, TModel>(stateComparer, triggerComparer);
        }

        #endregion
    }

    /// <summary>
    /// Provides a builder pattern for building a <see cref="StateMachineFactory&lt;TState, TTrigger, TModel&gt;"/>, which itself can produce zero or more instances of <see cref="StateMachine&lt;TState, TTrigger, TModel&gt;"/>.
    /// 
    /// This is not considered to be thread-safe.
    /// </summary>
    /// <typeparam name="TState">The type used to represent the state of the machine.</typeparam>
    /// <typeparam name="TTrigger">The type used to represent triggers which cause transitions within the machine.</typeparam>
    /// <typeparam name="TModel">The type which the machine will carry with it and can be used to store information or react differently.</typeparam>
    public sealed partial class StateMachineBuilder<TState, TTrigger, TModel>
    {
        #region Private fields

        private readonly Dictionary<TState, StateBuilder> _stateBuilders;
        private readonly Dictionary<TTrigger, Type[]> _triggerParameterTypes;
        private readonly IEqualityComparer<TState> _stateComparer;
        private readonly IEqualityComparer<TTrigger> _triggerComparer;
        private Action<StateMachine<TState, TTrigger, TModel>.Transition> _onUnhandledTrigger;

        #endregion

        #region Internal constructor

        /// <summary>
        /// Instantiate a new state machine builder.
        /// </summary>
        /// <param name="stateComparer">A comparer to test whether two states are equivalent. Can be null.</param>
        /// <param name="triggerComparer">A comparer to test whether two triggers are equivalent. Can be null.</param>
        internal StateMachineBuilder(IEqualityComparer<TState> stateComparer, IEqualityComparer<TTrigger> triggerComparer)
        {
            if (stateComparer == null)
            {
                stateComparer = EqualityComparer<TState>.Default;
            }
            if (triggerComparer == null)
            {
                triggerComparer = EqualityComparer<TTrigger>.Default;
            }

            _stateComparer = stateComparer;
            _triggerComparer = triggerComparer;
            _stateBuilders = new Dictionary<TState, StateBuilder>(stateComparer);
            _triggerParameterTypes = new Dictionary<TTrigger, Type[]>();
            _onUnhandledTrigger = null;
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Create a new <see cref="StateMachineFactory&lt;TState, TTrigger, TModel&gt;"/> that can be used to generate instances of <see cref="StateMachine&lt;TState, TTrigger, TModel&gt;"/>.
        /// </summary>
        /// <returns>A <see cref="StateMachineFactory&lt;TState, TTrigger, TModel&gt;"/>.</returns>
        public StateMachineFactory<TState, TTrigger, TModel> CreateFactory()
        {
            return new StateMachineFactory<TState, TTrigger, TModel>(
                new FrozenDictionary<TState, StateMachine<TState, TTrigger, TModel>.StateData>(
                    _stateBuilders
                        .Select(
                            pair => new KeyValuePair<TState, StateMachine<TState, TTrigger, TModel>.StateData>(
                                pair.Key,
                                pair.Value.ToStateData())),
                    _stateComparer),
                _stateComparer,
                _triggerComparer,
                _onUnhandledTrigger,
                new FrozenDictionary<TTrigger, Type[]>(
                    _triggerParameterTypes,
                    _triggerComparer));
        }

        /// <summary>
        /// Return a <see cref="StateBuilder"/> for the provided <paramref name="state"/>.
        /// 
        /// If this has been called for the provided <paramref name="state"/> before, it will return the same object.
        /// </summary>
        /// <param name="state">The state to configure.</param>
        /// <returns>A <see cref="StateBuilder"/></returns>
        /// <exception cref="System.ArgumentNullException">
        /// Thrown when <paramref name="state"/> is null.
        /// </exception>
        public StateBuilder Configure(TState state)
        {
            if (state == null)
            {
                throw new ArgumentNullException("state");
            }

            StateBuilder stateBuilder;
            if (!_stateBuilders.TryGetValue(state, out stateBuilder))
            {
                _stateBuilders[state] = stateBuilder = new StateBuilder(this, state);
            }

            return stateBuilder;
        }

        /// <summary>
        /// Set a callback to be invoked if a trigger is unhandled by a state.
        /// </summary>
        /// <param name="callback">A function to be invoked</param>
        /// <returns>The current <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/>, allowing for a fluent syntax</returns>
        /// <exception cref="System.ArgumentNullException">
        /// Thrown when <paramref name="callback"/> is null.
        /// </exception>
        /// <exception cref="System.InvalidOperationException">
        /// Thrown when OnUnhandledTrigger is called more than once.
        /// </exception>
        public StateMachineBuilder<TState, TTrigger, TModel> OnUnhandledTrigger(Action<StateMachine<TState, TTrigger, TModel>.Transition> callback)
        {
            if (callback == null)
            {
                throw new ArgumentNullException("callback");
            }

            if (_onUnhandledTrigger != null)
            {
                throw new InvalidOperationException("Already specified an OnUnhandledTrigger callback");
            }

            _onUnhandledTrigger = callback;

            return this;
        }

        /// <summary>
        /// Set the parameter types that must be passed in when a trigger is fired or accessed.
        /// </summary>
        /// <typeparam name="TArg1">The first trigger parameter type</typeparam>
        /// <param name="trigger">The trigger to affect</param>
        /// <returns>The current <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/>, allowing for a fluent syntax</returns>
        /// <exception cref="System.ArgumentNullException">
        /// Thrown when <paramref name="trigger"/> is null.
        /// </exception>
        /// <exception cref="System.ArgumentException">
        /// Thrown when <paramref name="trigger"/> has already had trigger parameters specified.
        /// </exception>
        public StateMachineBuilder<TState, TTrigger, TModel> SetTriggerParameters<TArg1>(TTrigger trigger)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            if (_triggerParameterTypes.ContainsKey(trigger))
            {
                throw new ArgumentException("Aready specified trigger parameters", "trigger");
            }

            _triggerParameterTypes[trigger] = new[] { typeof(TArg1) };

            return this;
        }

        /// <summary>
        /// Set the parameter types that must be passed in when a trigger is fired or accessed.
        /// </summary>
        /// <typeparam name="TArg1">The first trigger parameter type</typeparam>
        /// <typeparam name="TArg2">The second trigger parameter type</typeparam>
        /// <param name="trigger">The trigger to affect</param>
        /// <returns>The current <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/>, allowing for a fluent syntax</returns>
        /// <exception cref="System.ArgumentNullException">
        /// Thrown when <paramref name="trigger"/> is null.
        /// </exception>
        /// <exception cref="System.ArgumentException">
        /// Thrown when <paramref name="trigger"/> has already had trigger parameters specified.
        /// </exception>
        public StateMachineBuilder<TState, TTrigger, TModel> SetTriggerParameters<TArg1, TArg2>(TTrigger trigger)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            if (_triggerParameterTypes.ContainsKey(trigger))
            {
                throw new ArgumentException("Aready specified trigger parameters", "trigger");
            }

            _triggerParameterTypes[trigger] = new[] { typeof(TArg1), typeof(TArg2) };

            return this;
        }

        /// <summary>
        /// Set the parameter types that must be passed in when a trigger is fired or accessed.
        /// </summary>
        /// <typeparam name="TArg1">The first trigger parameter type</typeparam>
        /// <typeparam name="TArg2">The second trigger parameter type</typeparam>
        /// <typeparam name="TArg3">The third trigger parameter type</typeparam>
        /// <param name="trigger">The trigger to affect</param>
        /// <returns>The current <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/>, allowing for a fluent syntax</returns>
        /// <exception cref="System.ArgumentNullException">
        /// Thrown when <paramref name="trigger"/> is null.
        /// </exception>
        /// <exception cref="System.ArgumentException">
        /// Thrown when <paramref name="trigger"/> has already had trigger parameters specified.
        /// </exception>
        public StateMachineBuilder<TState, TTrigger, TModel> SetTriggerParameters<TArg1, TArg2, TArg3>(TTrigger trigger)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            if (_triggerParameterTypes.ContainsKey(trigger))
            {
                throw new ArgumentException("Aready specified trigger parameters", "trigger");
            }

            _triggerParameterTypes[trigger] = new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3) };

            return this;
        }

        /// <summary>
        /// Set the parameter types that must be passed in when a trigger is fired or accessed.
        /// </summary>
        /// <typeparam name="TArg1">The first trigger parameter type</typeparam>
        /// <typeparam name="TArg2">The second trigger parameter type</typeparam>
        /// <typeparam name="TArg3">The third trigger parameter type</typeparam>
        /// <typeparam name="TArg4">The fourth trigger parameter type</typeparam>
        /// <param name="trigger">The trigger to affect</param>
        /// <returns>The current <see cref="StateMachineBuilder&lt;TState, TTrigger, TModel&gt;"/>, allowing for a fluent syntax</returns>
        /// <exception cref="System.ArgumentNullException">
        /// Thrown when <paramref name="trigger"/> is null.
        /// </exception>
        /// <exception cref="System.ArgumentException">
        /// Thrown when <paramref name="trigger"/> has already had trigger parameters specified.
        /// </exception>
        public StateMachineBuilder<TState, TTrigger, TModel> SetTriggerParameters<TArg1, TArg2, TArg3, TArg4>(TTrigger trigger)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            if (_triggerParameterTypes.ContainsKey(trigger))
            {
                throw new ArgumentException("Aready specified trigger parameters", "trigger");
            }

            _triggerParameterTypes[trigger] = new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3), typeof(TArg4) };

            return this;
        }

        #endregion

        #region Internal methods

        /// <summary>
        /// Return the current configuration of the provided <paramref name="state"/> or null if it has not been configured by the user yet.
        /// </summary>
        /// <param name="state">The state to check for</param>
        /// <returns>A <see cref="StateBuilder"/> or null</returns>
        internal StateBuilder GetConfiguration(TState state)
        {
            StateBuilder stateBuilder;
            if (!_stateBuilders.TryGetValue(state, out stateBuilder))
            {
                return null;
            }

            return stateBuilder;
        }

        internal void ValidateTriggerParameters(TTrigger trigger, string paramName)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            Type[] typeArray;
            if (_triggerParameterTypes.TryGetValue(trigger, out typeArray))
            {
                throw new ArgumentException(string.Format("Expected {0} type parameters, got 0", typeArray.Length), "paramName");
            }
        }

        internal void ValidateTriggerParameters<TArg1>(TTrigger trigger, string paramName)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            Type[] typeArray;
            if (!_triggerParameterTypes.TryGetValue(trigger, out typeArray) || typeArray.Length != 1)
            {
                throw new ArgumentException(string.Format("Expected {0} type parameters, got 1", typeArray.Length), "paramName");
            }

            if (typeArray[0] != typeof(TArg1))
            {
                throw new ArgumentException(string.Format("Expected type parameter of {0}, got {1}", typeArray[0].FullName, typeof(TArg1).FullName), "paramName");
            }
        }

        internal void ValidateTriggerParameters<TArg1, TArg2>(TTrigger trigger, string paramName)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            Type[] typeArray;
            if (!_triggerParameterTypes.TryGetValue(trigger, out typeArray) || typeArray.Length != 2)
            {
                throw new ArgumentException(string.Format("Expected {0} type parameters, got 2", typeArray.Length), "paramName");
            }

            if (typeArray[0] != typeof(TArg1))
            {
                throw new ArgumentException(string.Format("Expected 1st type parameter of {0}, got {1}", typeArray[0].FullName, typeof(TArg1).FullName), "paramName");
            }
            else if (typeArray[1] != typeof(TArg2))
            {
                throw new ArgumentException(string.Format("Expected 2nd type parameter of {0}, got {1}", typeArray[1].FullName, typeof(TArg2).FullName), "paramName");
            }
        }

        internal void ValidateTriggerParameters<TArg1, TArg2, TArg3>(TTrigger trigger, string paramName)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            Type[] typeArray;
            if (!_triggerParameterTypes.TryGetValue(trigger, out typeArray) || typeArray.Length != 3)
            {
                throw new ArgumentException(string.Format("Expected {0} type parameters, got 3", typeArray.Length), "paramName");
            }

            if (typeArray[0] != typeof(TArg1))
            {
                throw new ArgumentException(string.Format("Expected 1st type parameter of {0}, got {1}", typeArray[0].FullName, typeof(TArg1).FullName), "paramName");
            }
            else if (typeArray[1] != typeof(TArg2))
            {
                throw new ArgumentException(string.Format("Expected 2nd type parameter of {0}, got {1}", typeArray[1].FullName, typeof(TArg2).FullName), "paramName");
            }
            else if (typeArray[2] != typeof(TArg3))
            {
                throw new ArgumentException(string.Format("Expected 3nd type parameter of {0}, got {1}", typeArray[2].FullName, typeof(TArg3).FullName), "paramName");
            }
        }

        internal void ValidateTriggerParameters<TArg1, TArg2, TArg3, TArg4>(TTrigger trigger, string paramName)
        {
            if (trigger == null)
            {
                throw new ArgumentNullException("trigger");
            }

            Type[] typeArray;
            if (!_triggerParameterTypes.TryGetValue(trigger, out typeArray) || typeArray.Length != 4)
            {
                throw new ArgumentException(string.Format("Expected {0} type parameters, got 4", typeArray.Length), "paramName");
            }

            if (typeArray[0] != typeof(TArg1))
            {
                throw new ArgumentException(string.Format("Expected 1st type parameter of {0}, got {1}", typeArray[0].FullName, typeof(TArg1).FullName), "paramName");
            }
            else if (typeArray[1] != typeof(TArg2))
            {
                throw new ArgumentException(string.Format("Expected 2nd type parameter of {0}, got {1}", typeArray[1].FullName, typeof(TArg2).FullName), "paramName");
            }
            else if (typeArray[2] != typeof(TArg3))
            {
                throw new ArgumentException(string.Format("Expected 3nd type parameter of {0}, got {1}", typeArray[2].FullName, typeof(TArg3).FullName), "paramName");
            }
            else if (typeArray[3] != typeof(TArg4))
            {
                throw new ArgumentException(string.Format("Expected 4th type parameter of {0}, got {1}", typeArray[3].FullName, typeof(TArg4).FullName), "paramName");
            }
        }

        #endregion
    }
}
